![]() |
![]() |
|
Die Vorgabe legt den Klassennamen auf Service1 fest. Die Klasse ist von ServiceBase abgeleitet, welche die Basisklasse der Windows-Dienste ist und zum Namespace System.ServiceProcess gehört. Wie jedes andere Programm wird auch ein Dienst durch den Aufruf der Methode Main gestartet. Was dabei passiert, ist bereits festgelegt. Zuerst wird ein Array vom Typ ServiceBase deklariert und diesem die Referenz auf das Dienst-Objekt vom Typ Service1 übergeben. Damit ist es auch möglich, innerhalb einer Windows-Dienstanwendung mehrere Dienste gleichzeitig bereitzustellen. Jeder Dienst ist dabei als eine Klasse definiert, die von ServiceBase abgeleitet ist. Die Initialisierung des Arrays muss dann nur durch die entsprechenden Objektreferenzen ergänzt werden. Im letzten Schritt wird die statische Methode Run der Klasse ServiceBase aufgerufen, und alle Dienste werden geladen. Ist die Methode ausgeführt, übernimmt der Service Control Manager die Steuerung der Dienste. Zwei geschützte Methoden der Basisklasse werden bereits überschrieben bereitgestellt: OnStart und OnStop. Hierbei handelt es sich um die Methoden, die ausgeführt werden, wenn der Dienst gestartet beziehungsweise beendet wird. OnStart enthält damit logischerweise die Funktionalität des Dienstes, in OnStop implementieren Sie den Code, der ausgeführt werden soll, wenn der Dienst beendet wird. Bei OnStart und OnStop handelt es sich genau genommen um Ereignisse, die aber von der Basisklasse nicht direkt veröffentlicht werden.
Angestoßen wird OnStart durch den Service Control Manager. Beim Booten des Systems ist das der Fall, wenn im Eigenschaftsdialog des Dienstes unter Starttyp Automatisch eingestellt ist (siehe auch Abbildung 24.2). Mit Manuell wird der Dienst beim Booten zwar in den Speicher geladen, wartet aber auf ein äußeres Signal, um seine Aufgaben auszuführen. Das kann durch Klicken der Schaltfläche Starten im Dialog erfolgen oder durch Programmcode. Analog verhält sich auch die Methode OnStop. Sie erhält einen Anstoß von außen, wenn der Dienst beendet wird – entweder durch Klicken der Schaltfläche Beenden oder mittels Programmcode. Wird ein Dienst gestartet, können ihm Startparameter übergeben werden, die im Dienste-Dialog im untersten Eingabefeld eingetragen werden (siehe Abbildung 24.2). Von der OnStart-Methode werden diese Parameter in einem String-Array entgegengenommen und können in der Methodenimplementierung ausgewertet werden, um das Laufzeitverhalten des Dienstes zu beeinflussen.
Die Methode OnStop hingegen ist parameterlos, ebenso wie alle anderen OnXxx-Methoden, die im folgenden Abschnitt beschrieben werden. 24.2.3 Die Methoden eines Dienstes
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Methode | Beschreibung |
| OnContinue | Wird ausgeführt, wenn der Dienst nach dem Anhalten wieder fortgesetzt wird. |
| OnPause | Wird ausgeführt, wenn der Dienst angehalten wird. |
| OnPowerEvent | Wird ausgeführt, wenn das System in den Stand-by-Modus geht. |
| OnStart | Wird ausgeführt, wenn der Dienst gestartet wird. |
| OnStop | Wird ausgeführt, wenn der Dienst beendet wird. |
| OnShutDowm | Wird ausgeführt, wenn das System heruntergefahren wird. |
Damit steht Ihnen eine Reihe von Methoden zur Verfügung, um das Verhalten eines Dienstes in allen denkbaren Situationen festzulegen. Welche Methoden überschrieben werden müssen, hängt von der Arbeitsweise des Dienstes ab. Mit Sicherheit werden Sie aber immer OnStart implementieren, damit der Dienst eine nützliche Funktionalität aufweist.
Der SCM sendet Signale an den Dienst, die zur Folge haben, dass dieser darauf entsprechend reagiert. Beim Starten ist es ein Signal, das die Ausführung von OnStart im Dienst nach sich zieht, beim Beenden ein Signal, infolge dessen OnStop aufgerufen wird. Analog gilt das auch für die anderen Methoden. Auf einem anderen Blatt steht jedoch, ob der Dienst überhaupt in der Lage ist, das Signal zu empfangen und an die passende Methode weiterzuleiten. Diese Festlegung erfolgt in den Eigenschaften der von ServiceBase abgeleiteten Klasse. Um sich die Eigenschaften im Eigenschaftsfenster anzeigen zu lassen, müssen Sie den Designer der Dienstklasse aktivieren.
Insgesamt vier Eigenschaften legen die Empfangsbereitschaft eines Dienstes fest:
| CanHandlePowerEvent |
| CanPauseAndContinue |
| CanShutDown |
| CanStop |
Bis auf CanStop sind alle genannten Eigenschaften mit False vorinitialisiert. Die Konsequenzen lassen sich am besten erkennen, wenn Sie sich den Eigenschaftsdialog eines Dienstes ansehen. In der Abbildung 24.2 können die beiden Schaltflächen Anhalten und Fortsetzen nur dann aktiviert werden, wenn der Dienst gestartet und die Eigenschaft CanPauseAndContinue=True festgelegt ist. Theoretisch wäre es möglich, einen Dienst zu starten, ohne ihn jemals beenden zu können. Dazu müsste die Eigenschaft CanStop=False eingestellt werden. In der Praxis werden Sie aber vermutlich nur selten auf einen Fall stoßen, der es erlaubt, auf Operationen beim Beenden eines Dienstes zu verzichten.
Ein Dienst kann seine Zustandsänderungen in das Anwendungsereignisprotokoll des Systems schreiben. Voraussetzung dazu ist, dass die Eigenschaft AutoLog des Dienstes True gesetzt ist. Das ist gleichzeitig auch die Voreinstellung, bedarf also keiner Änderung.

Hier klicken, um das Bild zu Vergrößern
Abbildung 24.3 Das »Anwendungsereignisprotokoll«
Im Anwendungsereignisprotokoll (siehe Abbildung 24.3) wird in der Spalte Quelle der Bezeichner ausgegeben, der durch die Eigenschaft ServiceName der Dienstklasse vorgegeben ist. Ein Doppelklick auf einen Eintrag öffnet das Eigenschaftsfenster des Ereignisses, in dem über die allgemeinen Angaben zum Ereignis hinaus auch noch eine Beschreibung angezeigt wird. Standardmäßig ist die Beschreibung leer. Wollen Sie allerdings einen informativen Text ausgeben, steht Ihnen mit EventLog eine öffentliche, schreibgeschützte Instanzeigenschaft der Dienstklasse zur Verfügung, welche die Referenz auf das Ereignisprotokoll zurückliefert.
Zur Übergabe einer Information an das Ereignisprotokoll dient die Methode WriteEntry des EventLog-Objekts, der im einfachsten Fall nur eine Zeichenfolge übergeben wird, z. B.:
| Protected Overrides Sub OnStart(ByVal args() As String) |
| Me.EventLog.WriteEntry("DiskWatcher gestartet") |
| ' weitere Anweisungen |
| End Sub |
In Abbildung 24.4 ist zu sehen, wie sich die Information im Eigenschaftsfenster der Ereignisses darstellt. Wenn Sie die Wirkung jetzt selbst ausprobieren wollen, müssen Sie zuerst eine Installationsdatei des Dienstes kompilieren. Dazu kommen wir im folgenden Abschnitt.
| Hinweis |
|
Um Ereigniseinträge in andere Systemprotokolldateien zu schreiben, bieten sich Ihnen über die Klasse EventLog Möglichkeiten dazu (siehe auch Abschnitt 8.2.4). In der Toolbox wird Ihnen zur Erleichterung Ihrer Arbeit für diese Klasse auch ein Control angeboten. |

Hier klicken, um das Bild zu Vergrößern
Abbildung 24.4 Eigenschaftsdialog eines Eintrags im Ereignisprotokoll
| Eigenschaften | Beschreibung |
| AutoLog | Gibt an, ob die Befehle zum Starten, Beenden, Anhalten und Fortsetzen im Ereignisprotokoll Anwendung aufgezeichnet werden sollen. |
| CanHandlePowerEvent | Gibt an, ab der Dienst Benachrichtigungen, die durch einen zu niedrigen Ladezustand der Akkus ausgelöst werden, verarbeiten kann. |
| CanPauseAndContinue | Gibt an, ob der Dienst angehalten und wieder fortgesetzt werden kann. |
| CanShutDown | Gibt an, ob der Dienst beim Herunterfahren des Systems benachrichtigt werden soll. |
| CanStop | Gibt an, ob der Dienst nach dem Starten beendet werden kann. |
| ServiceName | Gibt die Bezeichnung des Dienstes an. |
Damit hätten wir schon die Beschreibung der Bereitstellung eines Windows-Dienstes abgeschlossen, und es bleibt nur noch, den Dienst zu installieren. Damit der SCM den Dienst auch finden kann, muss er in die Registrierungsdatenbank eingetragen werden. Obwohl diese Aufgabe im ersten Moment schwierig erscheint, unterstützt uns das Visual Studio 2005 in dieser Hinsicht recht gut, denn dazu sind nur ein paar wenige Klicks und Einträge erforderlich.
Zwei Komponenten nehmen Ihnen einen Großteil der Arbeit ab: ServiceProcessInstaller und ServiceInstaller. Beide werden als Steuerelemente bereitgestellt, müssen jedoch zuvor der Toolbox hinzugefügt werden. Ziehen Sie beide Komponenten jeweils einmal in den Designer. ServiceProcessInstaller übernimmt die Installation der Installationsdatei des Dienstes auf dem System, ServiceInstaller trägt die Dienstkomponente in die Registry unter
| HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services |
In beiden Objekten sind nur wenige Eigenschaften bis zur endgültigen Fertigstellung der installationsfähigen Windows-Dienstanwendung festzulegen. Im ServiceProcessInstaller-Objekt ist das die Eigenschaft Account, mit der die Benutzerrechte des Dienstes bestimmt werden. In den meisten Fällen erweist sich LocalSystem als am besten geeignet.
| Anmerkung |
|
Weitere Informationen zu den drei weiteren Alternativen, den Sicherheitskontext eines Dienstes festzulegen, entnehmen Sie bitte der Online-Dokumentation. |
Im ServiceInstaller-Objekt sollten wir drei Eigenschaften berücksichtigen. Da wäre zuerst StartType zu nennen, mit der festgelegt wird, wie sich der Dienst nach dem Laden verhalten soll. Bekanntlich kann ein Dienst automatisch oder manuell gestartet werden. Die dritte sich anbietende Alternative wäre es, den Dienst zu deaktivieren, so dass er weder vom Benutzer noch durch ein Programm gestartet werden kann.
Einen weniger gravierenden Einfluss haben die beiden Eigenschaften DisplayName und ServiceName. Trotzdem sollten auch diese Eigenschaftseinstellungen sorgfältig vergeben werden. Der Text, den Sie unter DisplayName eintragen, wird im Dialog Dienste angezeigt, die Einstellung von ServiceName wird als Schlüssel in der Registry benutzt.
Damit sind alle vorbereitenden Arbeiten erledigt, und der Installer ist konfiguriert. Das Projekt muss anschließend nur noch kompiliert werden. Hier ist allerdings ein Hinweis notwendig, denn beim Kompilieren wird Ihnen eine Reihe von Kompilierfehlern angezeigt. Die Ursache liegt darin, dass in der Anwendung nicht automatisch auf die Assembly System.Configuration.Install.dll verwiesen wird. Sie müssen das manuell vornehmen. Das Ergebnis der Kompilierung ist eine EXE-Datei, die Installationsdatei des Windows-Dienstes.
Jetzt kommt es zur Installation auf dem Zielsystem. Dazu bieten sich zwei Alternativen an:
| eine Installationsroutine mit einem Weitergabeprojekt |
| das Tool InstallUtil.exe |
An dieser Stelle wollen wir auf die erstgenannte Möglichkeit verzichten. In Kapitel 28, in dem wir uns mit den Weitergabeprojekten noch ausgiebig beschäftigen, wird die Weitergabe eines Windows-Dienstes noch einmal Thema sein. Stattdessen wollen wir hier das Tool Install-Util.exe benutzen, das im Verzeichnis
| \Windows\Microsoft.NET\Framework\v<Versionsnummer> |
zu finden ist. Zum Installieren rufen Sie das Tool an der Konsole auf und übergeben als Parameter die Installationsdatei des Windows-Dienstes, also beispielsweise:
| InstallUtil FileWatchService.exe |
Ähnlich einfach kann ein Dienst auch deinstalliert werden. Dazu wird der Aufruf des Tools um den Optionsschalter /u ergänzt:
| InstallUtil /u FileWatchService.exe |
Wenn die Installation erfolgreich abgelaufen ist, werden Sie den selbst geschriebenen Windows-Dienst nun im Dienste-Dialog zu sehen bekommen.
Nun wollen wir unsere Kenntnisse einsetzen und einen Windows-Dienst vollständig implementieren. Dieser Dienst, dessen Name FileWatchService lautet, soll in der Lage sein, alle Änderungen am Dateisystem in einer Datei zu protokollieren, beispielsweise das Löschen, Ändern und Hinzufügen einer Datei. Außerdem soll der Dienst auch angehalten und erneut gestartet werden können.
Weil der Dienst permanent auf eingehende Änderungsmitteilungen lauscht und die OnStart-Methode das Zeitlimit von 30 Sekunden nicht überschreiten darf, wird die gesamte Funktionalität in der separaten Klasse Worker implementiert und in einem eigenen Thread ausgeführt.
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 24\FileWatchService |
| ' ---------------------------------------------------------- |
| Imports System.Threading |
| Public Class Service1 |
| Private myThread As Thread |
| Private path As String = "" |
| Protected Overrides Sub OnStart(ByVal args() As String) |
| If args.Length <> 0 Then |
| Me.path = args(0) |
| End If |
| Dim work As Worker = New Worker(Me.path) |
| myThread = New Thread(New ThreadStart( _ |
| AddressOf work.StartWorker)) |
| myThread.Start() |
| Me.EventLog.WriteEntry( _ |
| "FileWatchService wurde erfolgreich gestartet") |
| End Sub |
| Protected Overrides Sub OnStop() |
| myThread.Resume() |
| myThread.Abort() |
| myThread = Nothing |
| Me.EventLog.WriteEntry("FileWatchService wurde beendet") |
| End Sub |
| Protected Overrides Sub OnPause() |
| myThread.Suspend() |
| Me.EventLog.WriteEntry("FileWatchService wurde angehalten") |
| End Sub |
| Protected Overrides Sub OnContinue() |
| myThread.Resume() |
| Me.EventLog.WriteEntry("FileWatchService wurde fortgesetzt") |
| End Sub |
| End Class |
In OnStart wird zuerst ein Objekt vom Typ Worker bereitgestellt. Dem Konstruktor der Klasse wird eine Zeichenfolge übergeben, die eine Verzeichnisangabe enthält. In diesem und allen untergeordneten Verzeichnissen sollen die Änderungen protokolliert werden. Im Eigenschaftsdialog des Dienstes kann der Anwender das Verzeichnis im Eingabefeld Startparameter selbst bestimmen, ansonsten werden sämtliche Änderungen im Laufwerk C:\ festgehalten. Nach der Initialisierung des Arbeitsthreads und dem Aufruf der Startmethode schreibt OnStart nur noch eine Mitteilung in das Anwendungsereignisprotokoll.
In den anderen Methoden wird nur noch das Verhalten des Threads gesteuert: In OnPause wird der Thread in den Wartezustand versetzt, in OnContinue wird er bereit geschaltet. Das Stoppen des Dienstes durch Aufruf der Methode OnStop zerstört den Thread.
Sehen wir uns nun an, wie die Klasse Worker konstruiert ist.
| Imports System.Windows.Forms |
| Imports System.IO |
| Imports System.Collections.Specialized |
| Public Class Worker |
| Private protocolFile As String = Application.StartupPath & _ |
| "\FileWatchLog.log" |
| Private path As String = "c:\" |
| ' Konstruktor |
| Public Sub New(ByVal path As String) |
| If path <> "" Then |
| Me.path = path |
| End If |
| End Sub |
| ' Startmethode des Threads |
| Public Sub StartWorker() |
| Dim fsw As FileSystemWatcher = _ |
| New FileSystemWatcher(Me.path) |
| ' Überwachen aller Unterverzeichnisse einschließen |
| fsw.IncludeSubdirectories = True |
| ' Eventauslösung aktivieren |
| fsw.EnableRaisingEvents = True |
| ' Ereignishandler |
| AddHandler fsw.Changed, AddressOf ChangeFile |
| AddHandler fsw.Deleted, AddressOf DeleteFile |
| AddHandler fsw.Created, AddressOf CreateFile |
| AddHandler fsw.Renamed, AddressOf RenameFile |
| Try |
| ' in dieser Schleife wird auf etwaige Änderungen im |
| ' Dateisystem gewartet |
| Do While (True) |
| fsw.WaitForChanged(WatcherChangeTypes.All) |
| Loop |
| Catch |
| End Try |
| ' Eventauslösung deaktivieren |
| fsw.EnableRaisingEvents = False |
| End Sub |
| ' Datei wurde umbenannt |
| Private Sub RenameFile(ByVal sender As Object, _ |
| ByVal e As RenamedEventArgs) |
| Dim entry As String = "[" & _ |
| DateTime.Now.ToShortTimeString() & _ |
| "] Umbenannt: " & e.FullPath |
| WriteToLogFile(entry) |
| End Sub |
| ' Datei wurde gelöscht |
| Private Sub DeleteFile(ByVal sender As Object, _ |
| ByVal e As FileSystemEventArgs) |
| Dim entry As String = "[" & _ |
| DateTime.Now.ToShortTimeString() & _ |
| "] Gelöscht: " & e.FullPath |
| WriteToLogFile(entry) |
| End Sub |
| ' Datei wurde neu erzeugt |
| Private Sub CreateFile(ByVal sender As Object, _ |
| ByVal e As FileSystemEventArgs) |
| Dim entry As String = "[" & _ |
| DateTime.Now.ToShortTimeString() & _ |
| "] Erzeugt: " & e.FullPath |
| WriteToLogFile(entry) |
| End Sub |
| ' Datei wurde geändert |
| Private Sub ChangeFile(ByVal sender As Object, _ |
| ByVal e As FileSystemEventArgs) |
| ' Änderungen in der Protokolldatei nicht aufzeichnen |
| If (e.FullPath.ToLower() = protocolFile.ToLower()) Then |
| Return |
| End If |
| Dim entry As String = "[" & _ |
| DateTime.Now.ToShortTimeString() & _ |
| "] Geändert: " & e.FullPath |
| WriteToLogFile(entry) |
| End Sub |
| Private Function ReadFromLogFile() As StringCollection |
| Dim strCol As StringCollection = New StringCollection() |
| Dim sr As StreamReader = _ |
| New StreamReader(Me.protocolFile) |
| Dim line As String = "" |
| Dim count As Integer = 1 |
| Do While (sr.ReadLine() IsNot Nothing) |
| ' maximal 999 Einträge lesen |
| line = sr.ReadLine() |
| If (count = 999) Then |
| Exit Do |
| End If |
| strCol.Add(line) |
| count += 1 |
| Loop |
| sr.Close() |
| Return strCol |
| End Function |
| ' Schreibt eine Änderung in die Protokolldatei |
| Private Sub WriteToLogFile(ByVal entry As String) |
| Dim changedEntriesCol As StringCollection |
| If (File.Exists(Me.protocolFile)) Then |
| changedEntriesCol = ReadFromLogFile() |
| Else |
| changedEntriesCol = New StringCollection() |
| End If |
| ' neuen Eintrag an erster Stelle einfügen |
| changedEntriesCol.Insert(0, entry) |
| ' Schreibt Einträge in eine Log-Datei, dabei neue Datei |
| ' erzeugen oder alte überschreiben |
| Dim fs As FileStream = New FileStream(Me.protocolFile, _ |
| FileMode.Create) |
| Dim sw As StreamWriter = New StreamWriter(fs) |
| For Each str As String In changedEntriesCol |
| sw.WriteLine(str) |
| Next |
| sw.Close() |
| End Sub |
| End Class |
| Tipp |
|
Wenn Sie eine Klasse ähnlich Worker entwickeln, welche die Funktionalität eines Windows-Dienstes implementiert, sollten Sie die Klasse vorher außerhalb eines Windows-Dienstprojekts ausgiebig testen. Sie ersparen sich damit viel Zeit, die investiert werden müsste, um den geänderten Dienst immer wieder neu zu installieren und zu deinstallieren. |
Der gesamten Funktionalität des Dienstes liegt eine Klasse aus dem Namespace System.IO zugrunde: FileSystemWatcher. Auch diese Klasse wird Ihnen als Steuerelement in der Toolbox zur Verfügung gestellt. Objekte dieses Typs lösen Ereignisse aus, wenn eine Datei oder ein Verzeichnis verändert wird. Das Verzeichnis, ab dem überwacht werden soll, wird dem Konstruktor übergeben. Die Ereignisse, die ausgelöst werden, heißen Changed, Created, Deleted und Renamed.
Wird der Dienst gestartet und die OnStart-Methode aufgerufen, wird zuerst ein Objekt vom Typ der Klasse Worker erzeugt. Dabei wird das zu überwachende Verzeichnis übergeben und im Feld path gespeichert. Die Startmethode des Threads ist StartWorker, in der das FileSystemWatcher-Objekt erzeugt wird. Mit der Eigenschaft IncludeSubdirectories wird festgelegt, dass auch alle Unterverzeichnisse überwacht werden sollen, und mit EnableRaising-Events die Ereignisauslösung aktiviert. Nach dem Binden der Ereignishandler an die Events kommt der Kern des gesamten Dienstes: der Aufruf der Methode WaitForChanged des FileSystemWatcher-Objekts. Diese Methode wartet für einen unbegrenzten Zeitraum, bis eine Änderung eintritt, und gibt dann eine Rückgabe aus, die uns aber nicht weiter interessiert. Wichtig ist vielmehr, dass die Methode in einer Schleife lauert und das Beenden der Routine verhindert. Solange die Schleife nicht durch einen äußeren Einfluss, bei uns ist das der aufrufende Thread in der Dienstklasse, beendet wird, werden bei Änderungen am überwachten Verzeichnis die entsprechenden Ereignisse ausgelöst.
Aufgezeichnet werden die Veränderungen in einer Protokolldatei, die sich im gleichen Verzeichnis wie die ausführbare Datei des Dienstes befindet. In allen vier Ereignishandlern werden entsprechende Textinformationen über den Aufruf der benutzerdefinierten Methode WriteToLogFile in die Protokolldatei geschrieben. Allerdings müssen wir vorsichtig sein, was die Überwachung von Änderungen angeht. Befindet sich nämlich die Protokolldatei im überwachten Verzeichnis, wird sie natürlich selbst auch verändert und löst damit das Change-Ereignis erneut aus – der klassische Fall einer Endlosschleife. Daher wird im Ereignishandler ChangeFile zuerst geprüft, ob eine Änderung der Protokolldatei das Ereignis ausgelöst hat. In diesem Fall muss sich die Protokolldatei nicht selbst protokollieren.
Der in die Protokolldatei zu schreibende Eintrag wird der Methode WriteToLogFile übergeben. WriteToLogFile überprüft zuerst, ob die Protokolldatei bereits existiert. Ist das der Fall, wird sie mit der Methode ReadFromLogFile eingelesen. Jeder Eintrag in der Protokolldatei nimmt eine Zeile in Anspruch. Damit die Datei nicht unzulässig anwächst, ist die Maximalgröße im Programmcode auf 1000 Einträge beschränkt. Das wird dadurch erreicht, dass nur bis zu 999 Einträge eingelesen werden. Zur einfacheren Verwaltung wird jeder Dateieintrag einem StringCollection-Objekt übergeben. Der neue Eintrag wird danach mit dem Index 0 in die Auflistung geschoben. Damit ist garantiert, dass ein Anwender nach dem Öffnen der Protokolldatei die aktuellsten Einträge immer oben findet.
| << zurück |
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Copyright © Galileo Press 2007
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken.
Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die
gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich
geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung,
Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.